#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>

typedef struct msgbuf {
    long mtype;
    pid_t pid;
    int rid;
    key_t key;
} message_buf;

#define MAIN_KEY 99999
#define MSGSZ (sizeof(message_buf) - sizeof(long))

#define REQUEST_MTYPE 1
#define CONNECT_MTYPE 6
#define DISCONNECT_MTYPE 17

typedef struct _shmdata
{
    int ready;
    int value;
} shm_data;

#define SHM_SIZE (sizeof(shm_data))

typedef struct _client {
    pid_t pid;
    int prio;
} client_info;

int clients_number = 0;
client_info clients[1024];
pid_t fraction[100];

int request_id = 0;


extern int domain_init(){
    
    int msqid;
    
    if ((msqid = msgget(MAIN_KEY, 0666)) >= 0 ) {
        if( msgctl( msqid, IPC_RMID, 0) == -1) {
            return(-1);
        }
    }
    
    if((msqid = msgget(MAIN_KEY, IPC_CREAT | 0666)) < 0){
        fprintf(stderr, "msgget init error in domain_init\n");
        return -1;
    }
    
    return 0;
}

extern int domain_finalize(){
    
    int msqid;
    
    if ( (msqid = msgget(MAIN_KEY, 0666)) >= 0 ) {
        if( msgctl( msqid, IPC_RMID, 0) == -1) {
            return(-1);
        }
    }
    
    
    return 0;
}

/**
 * shared memory functions
 */

int shm_create(key_t key) {
    if ( shmget(key, SHM_SIZE, IPC_CREAT | 0666 ) == -1 ) {
        perror("shmget");
        return -1;
    }
}

int shm_write_value(key_t key, int value){
    
    int shmid;
    shm_data *data;
    
    if ((shmid = shmget(key, SHM_SIZE, 0666 )) == -1) {
        perror("shmget");
        return -1;
    }
    
    if ((data = shmat(shmid, (void *)0, 0)) == (shm_data *)(-1)) {
        perror("shmat");
        return -1;
    }
    
    data->ready = 1;
    data->value = value;
    
    /* detach from the segment */
    if (shmdt(data) == -1) {
        perror("shmdt");
        return -1;
    }
    
    return 0;
}

int shm_read_value(key_t key, int *result) {
    
    int shmid;
    shm_data *data;
    
    if ((shmid = shmget(key, SHM_SIZE, 0666 )) < 0) {
        perror("shmget");
        return -1;
    }
    
    if ((data = shmat(shmid, NULL, 0)) == (shm_data *) -1) {
        perror("shmat");
        return -1;
    }
    
    if ( data->ready ) { // server has written the value

        *result = data->value;

        /* detach from the segment: */
        if (shmdt(data) == -1) {
            perror("shmdt");
            return -1;
        }
        
        /* remove the segment */
        if (shmctl(shmid, IPC_RMID, NULL) == -1) {
            perror("shmctl");
            return -1;
        }

        return 0;
    } else {

        /* detach from the segment: */
        if (shmdt(data) == -1) {
            perror("shmdt");
            return -1;
        }

        return 1;
    }
}

/**
 * server functions
 */

void update_fraction() {
    int i, fraction_val, prio_sum = 0, fraction_index = 0;
    
    for (i = 0; i < clients_number; i++) {
        prio_sum += clients[i].prio;
    }
    
    for (i = 0; i < clients_number; i++) {
        fraction_val = 100 * clients[i].prio / prio_sum + fraction_index;
        
        for (; fraction_index < fraction_val; fraction_index++) {
            fraction[fraction_index] = clients[i].pid;
        }
    }
    
    for (; fraction_index < 100; fraction_index++) {
        fraction[fraction_index] = clients[clients_number-1].pid;
    }
}

int random_value(int min, int max) {
    return ((double)rand() / ((unsigned long)RAND_MAX+1)) * (max-min+1) + min;
}

pid_t random_client() {
    int random_val = random_value(0, 99);
    return fraction[random_val];
}

void remove_client(pid_t pid) {
    int i, j;
    
    for (i = 0; i < clients_number; i++) {
        if ( clients[i].pid == pid ) break;
    }
    
    for (j = i; j < clients_number-1; j++) {
        clients[j] = clients[j+1];
    }
    
    clients_number--;
}

void handle_msg_from_main_queue(message_buf rbuf) {
    if ( rbuf.mtype == CONNECT_MTYPE ) { // add process
        clients[clients_number].pid = rbuf.pid;
        clients[clients_number].prio = rbuf.rid;
        clients_number++;
    } else if ( rbuf.mtype == DISCONNECT_MTYPE ) { // remove process
        remove_client(rbuf.pid);
    }
}

int find_the_best_to_execute(){
    
    int msqid_main;
    message_buf rbuf;
    int ori_clients_number = clients_number;
    
    if((msqid_main = msgget(MAIN_KEY, 0666)) < 0){
        fprintf(stderr, "msgget error\n");
        exit(2);
    }
    
    while (1) {
        // if no clients, block until one message arrives
        if( !clients_number ) {
            if (msgrcv(msqid_main, &rbuf, MSGSZ, 0, NULL) < 0){
                perror("msgrcv");
                fprintf(stderr, "error when msgrcv\n");
                exit(1);
            }
            handle_msg_from_main_queue(rbuf);
        }
    
        // no block
        while ( msgrcv(msqid_main, &rbuf, MSGSZ, 0, IPC_NOWAIT) >= 0 ) {
            handle_msg_from_main_queue(rbuf);
        }

        if ( clients_number > 0 ) break;
    }

    if ( ori_clients_number != clients_number ) {
        update_fraction();
    }
    
    return random_client();
}

extern void start_service() {
    
    int msqid;
    message_buf rbuf;
    
    pid_t cnt_pid;
    pid_t srv_pid = getpid();
    
    int result;
    
    printf("[System] Starting Server...\n");
    
    srand(time(NULL));
    
    while(1) {
        
        cnt_pid = find_the_best_to_execute();
        
        if((msqid = msgget(cnt_pid, 0666)) < 0){
            perror("msgget");
            fprintf(stderr, "start_service msgget error\n");
            exit(2);
        }
        
        // if no msg available, no block here
        if(msgrcv(msqid, &rbuf, MSGSZ, REQUEST_MTYPE, IPC_NOWAIT) < 0){
            continue;
        }
        
        printf("[Server %d] Handle %d[%d].\n", srv_pid, rbuf.pid, rbuf.rid);
        
        result = rand();
        sleep(1);
        
        if (shm_write_value(rbuf.key, result) < 0) {
            fprintf(stderr, "error when write value to shared memory.\n");
            exit(1);
        }
    }
}

extern void stop_service(pid_t pid) {
    kill(pid, SIGTERM);
    printf("[System] Server stopped.\n");
}


/**
 * client connect functions
 */

extern void connect_service(int priority) {
    int msqid, msqid_main;
    message_buf sbuf;
    int pid = getpid();

    // create request queue for process
    if((msqid = msgget(pid, IPC_CREAT | 0666)) < 0){
        fprintf(stderr, "msgget use error in message_sent\n");
        exit(1);
    }

    // get process queue
    if((msqid_main = msgget(MAIN_KEY, 0666)) < 0){
        fprintf(stderr, "msgget use error in message_sent\n");
        exit(1);
    }

    sbuf.mtype = CONNECT_MTYPE; // used for connect
    sbuf.key = 0;
    sbuf.pid = pid;
    //use rid as priority
    sbuf.rid = priority;

    if(msgsnd(msqid_main, &sbuf, MSGSZ, IPC_NOWAIT) < 0){
        sprintf(stderr, "msgsnd send error in message_sent (msqid:%d, sbuf.mtype:%lu, sbuf_key:%lu, sbuf_pid:%d, sbuf_rid:%d)\n",
                msqid, sbuf.mtype, sbuf.key, sbuf.pid, sbuf.rid);
        exit(1);
    }

    printf("[Client %d] Started with priority %d.\n", pid, priority);
}

extern void disconnect_service() {
    int msqid, msqid_main;
    message_buf sbuf;
    int pid = getpid();
    
    // get process queue
    if((msqid_main = msgget(MAIN_KEY, 0666)) < 0){
        fprintf(stderr, "msgget use error in message_sent\n");
        exit(1);
    }
    
    sbuf.mtype = DISCONNECT_MTYPE; // used for disconnect
    sbuf.key = 0;
    sbuf.pid = pid;
    sbuf.rid = 0;
    
    if(msgsnd(msqid_main, &sbuf, MSGSZ, IPC_NOWAIT) < 0){
        sprintf(stderr, "msgsnd send error in message_sent (msqid:%d, sbuf.mtype:%lu, sbuf_key:%lu, sbuf_pid:%d, sbuf_rid:%d)\n",
                msqid, sbuf.mtype, sbuf.key, sbuf.pid, sbuf.rid);
        exit(1);
    }
    
    sleep(1);

    // remove request queue for process
    if((msqid = msgget(pid, 0666)) >= 0){
        if( msgctl( msqid, IPC_RMID, 0) == -1) {
            perror("msgctl");
            exit(1);
        }
    }
}

/**
 * client request functions
 */

key_t generate_key(int pid, int rid) {
    // TODO
    return ((long)pid) * 1024 + rid;
}

extern int blocking_request(char *argu, int *result){
    
    int msqid;
    message_buf sbuf;
    int pid = getpid();
    int read_status;
    
    
    //printf("[Client %d] Sending blocking %d[%d]\n", pid, pid, request_id);
    
    if((msqid = msgget(pid, 0666)) < 0){
        fprintf(stderr, "msgget use error in message_sent\n");
        return -1;
    }
    
    sbuf.mtype = REQUEST_MTYPE;
    sbuf.key = generate_key(pid, request_id);
    sbuf.pid = pid;
    sbuf.rid = request_id;
    
    request_id++;
    
    shm_create(sbuf.key);

    if(msgsnd(msqid, &sbuf, MSGSZ, IPC_NOWAIT) < 0){
        sprintf(stderr, "msgsnd send error in message_sent (msqid:%d, sbuf.mtype:%lu, sbuf_key:%lu, sbuf_pid:%d, sbuf_rid:%d)\n",
                msqid, sbuf.mtype, sbuf.key, sbuf.pid, sbuf.rid);
        return -1;
    }
    
    // wait for result
    while (1) {
        sleep(1);
        read_status = shm_read_value(sbuf.key, result);
        
        if ( read_status == 1 ) continue;
        if ( read_status == -1 ) return -1;
        if ( read_status == 0 ) {
            //printf("[Client %d] Receive result %d for request %d.\n", pid, *result, sbuf.rid);
            return 0;
        }
    }
}


extern int wait_for_results(key_t handle){
    
    int msqid;
    message_buf sbuf;
    size_t buf_length;
    int pid = getpid();
    int read_status;
    int result;
    
    while (1) {
        sleep(1);
        read_status = shm_read_value(handle, &result);
        
        if ( read_status == 1 ) continue;
        if ( read_status == -1 ) return -1;
        if ( read_status == 0 ) {
            //printf("[Client %d] Receive result %d.\n", pid, result);
            return result;
        }
    }
    
}

extern key_t unblocking_request(char *argu, int *result){
    
    int msqid;
    message_buf sbuf;
    int pid = getpid();
    
    //printf("[Client %d] Sending unblocking %d[%d]\n", pid, pid, request_id);
    
    if((msqid = msgget(pid, 0666)) < 0){
        fprintf(stderr, "msgget use error in message_sent\n");
        return -1;
    }
    
    sbuf.mtype = REQUEST_MTYPE;
    sbuf.key = generate_key(pid, request_id);
    sbuf.pid = pid;
    sbuf.rid = request_id;
    
    request_id++;
    
    shm_create(sbuf.key);

    if(msgsnd(msqid, &sbuf, MSGSZ, IPC_NOWAIT) < 0){
        sprintf(stderr, "msgsnd send error in message_sent (msqid:%d, sbuf.mtype:%lu, sbuf_key:%lu, sbuf_pid:%d, sbuf_rid:%d)\n",
                msqid, sbuf.mtype, sbuf.key, sbuf.pid, sbuf.rid);
        return -1;
    }
    
    return sbuf.key;
    
}
